# coding=utf-8
from __future__ import print_function, absolute_import

import os
import sys
from typing import List, NoReturn, Text

import akshare as ak
from gm.api import run, subscribe, unsubscribe, schedule, stop, get_unfinished_orders
from gm.csdk.c_sdk import BarLikeDict2, TickLikeDict2
from gm.enum import ADJUST_PREV, PositionEffect_Close, PositionEffect_Open, PositionSide_Long, MODE_LIVE
from gm.model import DictLikeAccountStatus, DictLikeExecRpt, DictLikeIndicator, DictLikeOrder, DictLikeParameter

from config import strategy_config

sys.path.append(strategy_config.python_quant_path)
from python_quant.common import constant
from python_quant.common.code import code_to_shse_szse_symbol, symbol_remove_shse_szse
from python_quant.common.log import get_logger
from python_quant.quant_api.juejin.juejin import get_his_n_data
from python_quant.quant_api.juejin.order import do_order_at_market_by_value, do_close_order_at_market_by_volume, \
    can_close_symbol, \
    can_open_symbol, had_position
from python_quant.stock.side.tech_side.tech_side import floor_out
from service import stock_pool, notify_mail

log = get_logger(os.path.split(__file__)[-1].split(".")[0])
# log = get_logger("li_run")

"""
本示例用于说明python sdk 当前支持的回调方法示例. 
不具有业务含义, 只用于策略编写参考
注：
建议使用python3.6.5以上的版本, gmsdk 支持Python3.6.x, python3.7.x, python3.8.x, python3.9.x 
"""


def init(context):
    # 定义全局常量示例
    # context.user_data = LoggerConfig.logger_name
    print('li执行器 init process %s.' % os.getpid())
    """
    策略中必须有init方法,且策略会首先运行init定义的内容，可用于
    * 获取低频数据(get_fundamentals, get_fundamentals_n, get_instruments, get_history_instruments, get_instrumentinfos,
    get_constituents, get_history_constituents, get_sector, get_industry, get_trading_dates, get_previous_trading_date,
    get_next_trading_date, get_dividend, get_continuous_contracts, history, history_n, )
    * 申明订阅的数据参数和格式(subscribe)，并附带数据事件驱动功能
    * 申明定时任务(schedule)，附带本地时间事件驱动功能
    * 读取静态的本地数据或第三方数据
    * 定义全局常量,如 context.user_data = 'balabala'
    * 最好不要在init中下单(order_volume, order_value, order_percent, order_target_volume, order_target_value, order_target_percent)
    """
    # subscribe_stock_pool()
    # 示例定时任务: 每天 14:50:00 调用名字为 my_schedule_task 函数
    # schedule(schedule_func=new_stock_Strategy.do_select(context), date_rule='1d', time_rule='20:00:00')
    # 示例订阅浦发银行，60s的频率
    # subscribe_stock_pool(context)
    schedule(schedule_func=create_stock_pool, date_rule='1d', time_rule='09:30:00')
    # schedule(schedule_func=stop_strategy, date_rule='1d', time_rule='02:06:00')


def on_tick(context, tick):
    # type: (Context, TickLikeDict2) -> NoReturn
    """
    tick数据推送事件
    参数 tick 为当前被推送的tick.
    tick包含的key值有下列值.
    symbol              str                   标的代码
    open                float                 日线开盘价
    high                float                 日线最高价
    low                 float                 日线最低价
    price               float	              最新价
    cum_volume          long                  成交总量/最新成交量,累计值
    cum_amount          float                 成交总金额/最新成交额,累计值
    trade_type          int                   交易类型 1: ‘双开’, 2: ‘双平’, 3: ‘多开’, 4: ‘空开’, 5: ‘空平’, 6: ‘多平’, 7: ‘多换’, 8: ‘空换’
    last_volume         int                   瞬时成交量
    cum_position        int                   合约持仓量(期),累计值（股票此值为0）
    last_amount         float                 瞬时成交额
    created_at          datetime.datetime     创建时间
    quotes              list[Dict]            股票提供买卖5档数据, list[0]~list[4]分别对应买卖一档到五档, 期货提供买卖1档数据, list[0]表示买卖一档. 目前期货的 list[1] ~ list[4] 值是没有意义的
        quotes 里每项包含的key值有:
          bid_p:  float   买价
          bid_v:  int     买量
          ask_p   float   卖价
          ask_v   int     卖量

    注: 可以使用属性访问的方式得到相应的key的值. 如要访问: symbol. 则可以使用 tick.symbol 或 tick['symbol']
    访问quote里的bid_p, 则可以使用 tick.quotes[0].bid_p  或 tick['quotes'][0]['bid_p']
    """
    pass


def do_trade(context, symbol):
    stock_info = stock_pool.select_last_add_all_stock_pool(symbol_remove_shse_szse(symbol))

    if stock_info is not None and had_position(context, symbol):
        stock_pool.update_stock_pool_had_position(constant.BOOL.TRUE, stock_info['id'])
    elif stock_info is not None and had_position(context, symbol) == False:
        stock_pool.update_stock_pool_had_position(constant.BOOL.FALSE, stock_info['id'])

    if (stock_info is None) or (
            stock_info['valid'] == constant.BOOL.FALSE and can_close_symbol(context, symbol) == False):
        unsubscribe(symbols=symbol, frequency='60s')
        return
    position = context.account().position(symbol=symbol, side=PositionSide_Long)
    if can_close_symbol(context, symbol):
        '''TODO:计算是否应该止损止盈'''
        if stock_info is not None and (
                position.fpnl >= stock_info['tp_fpnl'] or position.fpnl <= stock_info['sl_fpnl']):
            do_close_order_at_market_by_volume(symbol, position.volume)

    history_n_data = get_his_n_data(symbol=symbol, frequency='1d', count=1000000, end_time=context.now,
                                    adjust=ADJUST_PREV, with_new_tick_price=True)

    now_date = context.now.strftime("%H:%M:%S")
    if now_date < '14:50:00':
        return

    # 取收盘价序列
    C = history_n_data['close'].values

    # 取开盘价序列
    O = history_n_data['open'].values

    # 取最高价序列
    H = history_n_data['high'].values

    # 取最低价序列
    L = history_n_data['low'].values

    NEW_S, NEW_B = floor_out(C, H, L, O)
    if NEW_B[-1]:
        #   判断持仓单中是否存在，判断委托单中是否存在
        #   最大持仓股票数量不能超过10只
        #   每个标的持仓金额不能超过10万
        #   一次开仓4次加仓
        #   持仓标的数量最大10只
        #   获取和计算连损次数
        #   获取下单金额
        #   余额是否足够下单
        #   计算盈利比例，覆盖累计亏损所需涨幅，再加10%
        #   计算止损比例，对应的金额
        if stock_info['valid'] == '0':
            return
        if not can_open_symbol(context, symbol):
            return
        if len(context.account().positions()) >= 10:
            return
        if stock_info is None or stock_info['keep_loss_times'] >= stock_info['max_loss_times']:
            return
        keep_loss_times = stock_info['keep_loss_times']
        each_add_trade_amount = stock_info['each_add_trade_amount'].split(',')
        if keep_loss_times > len(each_add_trade_amount) - 1:
            keep_loss_times = len(each_add_trade_amount) - 1
        current_trade_amount = float(each_add_trade_amount[keep_loss_times])
        if context.account().cash.available < current_trade_amount:
            return
        do_order_at_market_by_value(symbol, current_trade_amount)

    if NEW_S[-1]:
        # 平仓持仓单
        # 计算连损次数，计算开仓仓位
        if not can_close_symbol(context, symbol):
            return

        if stock_info is not None:
            do_close_order_at_market_by_volume(symbol, position.volume)


def on_bar(context, bars):
    # type: (Context, List[BarLikeDict2]) -> NoReturn
    """
    bar数据推送事件
    参数 bars 为当前被推送的bar列表. 在调用subscribe时指定 wait_group=True, 则返回的是到当前已准备好的bar列表; 若 wait_group=False, 则返回的是当前推送的 bar 一个对象, 放在在 list 里
    bar 对象包含的key值有下列值.
    symbol         str      标的代码
    frequency      str      频率, 支持多种频率. 要把时间转换为相应的秒数. 如 30s, 60s, 300s, 900s
    open           float    开盘价
    close          float    收盘价
    high           float    最高价
    low            float    最低价
    amount         float    成交额
    volume         long     成交量
    position       long     持仓量（仅期货）
    bob            datetime.datetime    bar开始时间
    eob            datetime.datetime    bar结束时间

    注: 可以使用属性访问的方式得到相应的key的值. 如要访问: symbol. 则可以使用 bar.symbol 或 bar['symbol']
    """
    for bar in bars:
        do_trade(context, bar.symbol)


def on_order_status(context, order):
    # type: (Context, DictLikeOrder) -> NoReturn
    """
    委托状态更新事件. 参数order为委托信息
    响应委托状态更新事情，下单后及委托状态更新时被触发
    """
    if context.mode == 1:
        notify_mail.send_mail_thread("委托单状态发生变更", str(order))


def on_execution_report(context, execrpt):
    # type: (Context, DictLikeExecRpt) -> NoReturn
    """
    委托执行回报事件. 参数 execrpt 为执行回报信息
    响应委托被执行事件，委托成交后被触发
    """
    if context.mode == 1:
        notify_mail.send_mail_thread("成交回报状态发生变更", str(execrpt))
    stock_info = stock_pool.select_last_add_stock_pool(symbol_remove_shse_szse(execrpt.symbol))

    if execrpt.exec_type == 15 and stock_info is not None and stock_info['last_exec_id'] != execrpt.exec_id:
        if execrpt.position_effect == PositionEffect_Close:
            # 止盈
            if stock_info['exec_buy_price'] <= execrpt.price:
                stock_pool.update_stock_pool_trade_info(keep_loss_times=0, total_loss_amount=0, tp_fpnl=0,
                                                        sl_fpnl=0, last_exec_id=execrpt.exec_id, exec_buy_price=-1,
                                                        id=stock_info['id'])
            if stock_info['exec_buy_price'] > execrpt.price:
                # 止损
                stock_info['keep_loss_times'] += 1
                stock_info['total_loss_amount'] += round(
                    (stock_info['exec_buy_price'] - execrpt.price) * execrpt.volume, 2)
                stock_pool.update_stock_pool_trade_info(keep_loss_times=stock_info['keep_loss_times'],
                                                        total_loss_amount=stock_info['total_loss_amount'],
                                                        tp_fpnl=0,
                                                        sl_fpnl=0,
                                                        last_exec_id=execrpt.exec_id,
                                                        exec_buy_price=stock_info['exec_buy_price'],
                                                        id=stock_info['id'])

        if execrpt.position_effect == PositionEffect_Open:
            tp_fpnl = stock_info['total_loss_amount'] + (execrpt.amount * 0.1)
            sl_fpnl = -(execrpt.amount * 0.07)
            stock_pool.update_stock_pool_trade_info(keep_loss_times=stock_info['keep_loss_times'],
                                                    total_loss_amount=stock_info['total_loss_amount'],
                                                    tp_fpnl=tp_fpnl,
                                                    sl_fpnl=sl_fpnl,
                                                    last_exec_id=execrpt.exec_id,
                                                    exec_buy_price=execrpt.price,
                                                    id=stock_info['id'])


def on_account_status(context, account_status):
    # type: (Context, DictLikeAccountStatus) -> NoReturn
    """
    交易账户状态变更事件. 仅响应 已连接，已登录，已断开 和 错误 事件
    account_status: 包含account_id(账户id), account_name(账户名),ConnectionStatus(账户状态)
    """
    if context.mode == 1:
        notify_mail.send_mail_thread("交易账户状态发生变更", str(account_status))


def on_parameter(context, parameter):
    # type: (Context, DictLikeParameter) -> NoReturn
    """
    动态参数修改事件推送. 参数 parameter 为动态参数的信息
    """
    pass


def on_backtest_finished(context, indicator):
    # type: (Context, DictLikeIndicator) -> NoReturn
    """
    回测结束事件. 参数 indicator 为此次回测的绩效指标参数信息

    """
    print("on_backtest_finished")
    pass


def on_error(context, code, info):
    # type: (Context, int, Text) -> NoReturn
    """
    底层sdk出错时的回调函数
    :param context:
    :param code: 错误码.  参考: https://www.myquant.cn/docs/python/python_err_code
    :param info: 错误信息描述
    """
    code_info = str(code) + " " + str(info)
    if context.mode == 1:
        notify_mail.send_mail_thread("策略运行出错", code_info)


def on_trade_data_connected(context):
    # type: (Context) -> NoReturn
    """
    交易通道网络连接成功事件
    """
    if context.mode == 1:
        notify_mail.send_mail_thread("交易通道网络连接成功", "交易通道网络连接成功")


def on_market_data_connected(context):
    # type: (Context) -> NoReturn
    """
    实时行情网络连接成功事件
    """
    if context.mode == 1:
        notify_mail.send_mail_thread("实时行情网络连接成功", "实时行情网络连接成功")


def on_market_data_disconnected(context):
    # type: (Context) -> NoReturn
    """
    实时行情网络连接断开事件
    """
    if context.mode == 1:
        notify_mail.send_mail_thread("实时行情网络连接断开", "实时行情网络连接断开")


def on_trade_data_disconnected(context):
    # type: (Context) -> NoReturn
    """
    交易通道网络连接断开事件
    """
    if context.mode == 1:
        notify_mail.send_mail_thread("交易通道网络连接断开", "交易通道网络连接断开")


def on_shutdown(context):
    # type: (Context) -> NoReturn
    """
    策略退出前回调
    注：只有在终端点击策略·断开·按钮才会触发，直接关闭策略控制台不会被调用
    """
    if context.mode == 1:
        notify_mail.send_mail_thread("策略已经人工退出", "策略已经人工退出")


def create_stock_pool(context):
    unsubscribe(symbols='*')
    positions = context.account().positions()
    unfinished_orders = get_unfinished_orders()
    if len(positions) > 0 or len(unfinished_orders) > 0:
        return
    stock_pool_df = ak.stock_zh_a_spot_em()
    stock_pool_df = stock_pool_df[
        ((stock_pool_df['代码'].str.startswith('60')) | (stock_pool_df['代码'].str.startswith('30')) | (
            stock_pool_df['代码'].str.startswith('00'))) & (~stock_pool_df['名称'].str.contains('ST')) & (
                stock_pool_df['涨跌幅'] == -1) & (stock_pool_df['量比'] >= 1)]
    stock_pool_df = stock_pool_df.dropna()
    code_list = stock_pool_df['代码'].values
    full_code_list = list()
    for code in code_list:
        full_code = code_to_shse_szse_symbol(code)
        full_code_list.append(full_code)
    context.stock_pool = full_code_list
    subscribe(symbols=full_code_list, frequency='60s', count=1, unsubscribe_previous=True, wait_group=False)
    if context.mode == 1:
        notify_mail.send_mail_thread("股票池标的已经完成订阅", str(full_code_list))


def run_strategy():
    print("Running strategy")
    """
        strategy_id策略ID, 由系统生成
        filename文件名, 请与本文件名保持一致
        mode运行模式, 实时模式:MODE_LIVE回测模式:MODE_BACKTEST
        token绑定计算机的ID, 可在系统设置-密钥管理中生成
        backtest_start_time回测开始时间
        backtest_end_time回测结束时间
        backtest_adjust股票复权方式, 不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST
        backtest_initial_cash回测初始资金
        backtest_commission_ratio回测佣金比例
        backtest_slippage_ratio回测滑点比例
        """
    run(strategy_id=strategy_config.strategy_id,
        filename='li_run.py',
        mode=MODE_LIVE,
        token=strategy_config.token,
        backtest_start_time='2023-03-19 14:50:00',
        backtest_end_time='2023-03-21 16:00:00',
        backtest_adjust=ADJUST_PREV,
        backtest_initial_cash=10000000,
        backtest_commission_ratio=0.0001,
        backtest_slippage_ratio=0.0001)


def stop_strategy(context):
    stop()
    sys.exit()


if __name__ == '__main__':
    run_strategy()
